package service

import (
	"GopherFocus/app/dao"
	"GopherFocus/app/model"
	"GopherFocus/app/shared"
	"GopherFocus/app/system/index/internal/define"
	"context"
	"fmt"
	"github.com/gogf/gf/crypto/gmd5"
	"github.com/gogf/gf/database/gdb"
	"github.com/gogf/gf/errors/gerror"
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/util/gconv"
	"github.com/o1egl/govatar"
)

var User = userService{
	avatarUploadPath:      g.Cfg().GetString(`setting.path`) + `/avatar`,
	avatarUploadUrlPrefix: `/upload/avatar`,
}

type userService struct {
	avatarUploadPath      string // 头像上传路径
	avatarUploadUrlPrefix string // 头像上传对应的URL前缀
}

// 判断是否是管理员
func (s *userService) IsAdminShow(ctx context.Context, userId uint) bool {
	// 获取自定义上下文对象
	c := shared.Context.Get(ctx)
	if c == nil || c.User == nil || c.User.Id != userId {
		return false
	}
	return c.User.IsAdmin
}

// 检测给定的账号是否唯一
func (s *userService) CheckPassportUnique(ctx context.Context, passport string) error {
	// 在数据库中查询条数是否大于1
	n, err := dao.User.Ctx(ctx).Where(dao.User.C.Passport, passport).Count()
	if err != nil {
		return err
	}
	if n > 0 {
		return gerror.Newf(`账号"%s"已被占用`, passport)
	}
	return nil
}

// 检测给定的昵称是否唯一
func (s *userService) CheckNicknameUnique(ctx context.Context, nickname string) error {
	n, err := dao.User.Ctx(ctx).Where(dao.User.C.Nickname, nickname).Count()
	if err != nil {
		return err
	}
	if n > 0 {
		return gerror.Newf(`昵称"%s"已被占用`)
	}
	return nil
}

// 用户注册
func (s *userService) Register(ctx context.Context, input define.UserRegisterInput) error {
	return dao.User.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		var user *model.User
		if err := gconv.Struct(input, &user); err != nil {
			return err
		}
		if err := s.CheckPassportUnique(ctx, user.Passport); err != nil {
			return err
		}
		if err := s.CheckNicknameUnique(ctx, user.Nickname); err != nil {
			return err
		}
		user.Password = s.EncryptPassword(user.Passport, user.Password)
		// 自动生成头像
		avatarFilePath := fmt.Sprintf(`%s/%s.jpg`, s.avatarUploadPath, user.Passport)
		if err := govatar.GenerateFileForUsername(govatar.MALE, user.Passport, avatarFilePath); err != nil {
			return gerror.Wrapf(err, `自动创建头像失败`)
		}
		user.Avatar = fmt.Sprintf(`%s/%s.jpg`, s.avatarUploadUrlPrefix, user.Passport)
		// OmitEmpty会值为零值的字段过滤
		_, err := dao.User.Ctx(ctx).Data(user).OmitEmpty().Save()
		return err
	})
}

// 将密码按照内部算法进行加密
func (s *userService) EncryptPassword(passport, password string) string {
	return gmd5.MustEncrypt(passport + password)
}

// 根据账户密码查询用户信息
// 注意password参数传入的是按照相同加密算法加密后的密码字符串
func (s *userService) GetUserByPassportAndPassword(ctx context.Context, passport, password string) (user *model.User, err error) {
	err = dao.User.Ctx(ctx).Where(g.Map{
		dao.User.C.Passport: passport,
		dao.User.C.Password: password,
	}).Scan(&user)
	return
}

// 执行登录
func (s *userService) Login(ctx context.Context, input define.UserLoginInput) error {
	userEntity, err := s.GetUserByPassportAndPassword(
		ctx,
		input.Passport,
		s.EncryptPassword(input.Passport, input.Password),
	)
	if err != nil {
		return err
	}
	if userEntity == nil {
		return gerror.New(`帐号或密码错误`)
	}
	// 自动更新上线
	shared.Context.SetUser(ctx, &model.ContextUser{
		Id:       userEntity.Id,
		Passport: userEntity.Passport,
		Nickname: userEntity.Nickname,
		Avatar:   userEntity.Avatar,
	})
	return nil
}

// 获取用户主页的信息（包括个人信息，用户发布的Content列表，用户发布的Content的类型统计）
// 注意：如果在某一步err!=nil, 应该要返回output？？ 还是否返回nil, err？？ 因为上一步正确获取到了数据
func (s *userService) GetList(ctx context.Context, input define.UserGetListInput) (output *define.UserGetListOutput, err error) {
	output = &define.UserGetListOutput{}
	// 1.获取用户发布的Content内容
	output.Content, err = Content.GetList(ctx, input.ContentGetListInput)
	if err != nil {
		return output, err
	}

	// 2.根据用户ID获取用户个人信息
	output.User, err = s.GetProfileById(ctx, input.UserId)
	if err != nil {
		return output, err
	}

	// 3.根据用户ID获取Content的分类统计情况
	output.Stats, err = s.GetUserStats(ctx, input.UserId)
	if err != nil {
		return output, err
	}
	return
}

// 获取用户个人信息
func (s *userService) GetProfileById(ctx context.Context, userId uint) (output *define.UserGetProfileOutput, err error) {
	// 先为output创建一个实例
	output = &define.UserGetProfileOutput{}
	// 1.不必加上Fields，则默认取User模型的所有字段
	// 2.Scan方法会对参数进行字段匹配，类似于v.Struct(param)
	// 3.dao.User.Ctx(ctx).xxx  注意：第三层有一个Ctx，这个是必须要有的
	if err = dao.User.Ctx(ctx).Scan(&output); err != nil {
		return nil, err
	}
	return
}

// 获取Content分类统计结果 + 统计回复总数
// statsModel.All() 查询到的是一个切片，所以 for _, v := range statsAll 相当于遍历一个切片
func (s *userService) GetUserStats(ctx context.Context, userId uint) (map[string]int, error) {
	// Where在Group前面
	// 相当于: select type, count(*) as total from Content where UserId=userId group by type
	m := dao.Content.Ctx(ctx).Fields(dao.Content.C.Type, "count(*) total")
	// 不是管理员，则添加Where条件
	if userId > 0 && !s.IsAdminShow(ctx, userId) {
		m = m.Where(dao.Content.C.UserId, userId)
	}
	statsModel := m.Group(dao.Content.C.Type)
	statsAll, err := statsModel.All()
	if err != nil {
		return nil, err
	}
	statsMap := make(map[string]int)
	for _, v := range statsAll {
		statsMap[v["type"].String()] = v["total"].Int()
	}

	// 统计回复
	replyModel := dao.Reply.Ctx(ctx).Fields("count(*) total")
	// 判断是否是管理员
	if userId > 0 && !s.IsAdminShow(ctx, userId) {
		replyModel = replyModel.Where(dao.Reply.C.UserId, userId)
	}
	record, err := replyModel.One()
	if err != nil {
		return nil, err
	}
	// 用于/user/profile接口的"我的消息"功能
	statsMap["message"] = record["total"].Int()
	return statsMap, nil
}

// 获取用户个人信息
func (s *userService) GetProfile(ctx context.Context) (output *define.UserGetProfileOutput, err error) {
	// Shared.Context.Get(ctx)从当前上下文对象中获取自定义上下文对象，然后从自定义上下文对象中获取User信息
	return s.GetProfileById(ctx, shared.Context.Get(ctx).User.Id)
}

// 更新个人信息 修改操作需要在事务中进行
// 0.传入昵称和性别
// 1.通过上下文变量获取用户ID
// 2.根据用户ID查询是否存在相同昵称的数据
// 3.更新数据库User信息
// 4.更新Session信息
// 5.为什么不需要更新上下文变量中的User昵称
func (s *userService) UpdateProfile(ctx context.Context, input define.UserUpdateProfileInput) error {
	return dao.User.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		var (
			user   = shared.Context.Get(ctx).User
			userId = user.Id
		)
		// 非当前用户中，查找昵称等于input.Nickname的数据数量，如果大于0则表示已存在该昵称
		n, err := dao.User.Ctx(ctx).
			Where(dao.User.C.Nickname, input.Nickname).
			WhereNot(dao.User.C.Id, userId).
			Count()
		if err != nil {
			return err
		}
		if n > 0 {
			return gerror.Newf(`昵称"%s"已被占用`, input.Nickname)
		}
		// 更新数据库User信息
		// Data(input) 设置Model需要操作的数据
		// OmitEmpty() 空值过滤，对input中字段值为空字段进行过滤，即不会对为零值的字段进行更新操作
		_, err = dao.User.Ctx(ctx).OmitEmpty().Data(input).WherePri(dao.User.C.Id, userId).Update()
		if err != nil {
			return err
		}
		// 更新Session中的用户信息(能不能放到更新数据库User之前执行??? 如果昵称未修改则不更新数据库)
		if input.Nickname != user.Nickname {
			sessionUser := Session.GetUser(ctx)
			sessionUser.Nickname = input.Nickname
			err = Session.SetUser(ctx, sessionUser)
		}
		return err
	})
}

// 更新头像
// 如果dao.User.Transaction(ctx, func(xx) error {}) 返回的error不会nil，则会对事务进行回滚，否则提交事务
func (s *userService) UpdateAvatar(ctx context.Context, input define.UserUpdateAvatar) (err error) {
	return dao.User.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		var (
			err    error
			user   = shared.Context.Get(ctx).User
			userId = user.Id
		)
		// 这里为什么要先更新再判断照片是否相等??? 先判断照片是否相等，不相等再更新不是更好吗
		_, err = dao.User.Ctx(ctx).Data(input).
			Where(dao.User.C.Id, userId).
			Update()
		if err == nil && input.Avatar != user.Avatar {
			sessionUser := Session.GetUser(ctx)
			sessionUser.Avatar = input.Avatar
			err = Session.SetUser(ctx, sessionUser)
		}
		return err
	})
}

// 修改密码
func (s *userService) UpdatePassword(ctx context.Context, input define.UserPasswordInput) error {
	return dao.User.Ctx(ctx).Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		// 将帐号密码加密成新密码
		oldPassword := s.EncryptPassword(shared.Context.Get(ctx).User.Passport, input.OldPassword)
		// 查根据id查找到该用户，然后判断该用户密码是否等于oldPassword,如果n==0,说明不相等，则原始密码错误
		n, err := dao.User.Ctx(ctx).
			Where(dao.User.C.Id, shared.Context.Get(ctx).User.Id).
			Where(dao.User.C.Password, oldPassword).
			Count()
		if err != nil {
			return err
		}
		if n == 0 {
			return gerror.Newf(`原始密码错误`)
		}
		newPassword := s.EncryptPassword(shared.Context.Get(ctx).User.Passport, input.NewPassword)
		_, err = dao.User.Ctx(ctx).Data(g.Map{
			dao.User.C.Password: newPassword,
		}).Where(dao.User.C.Id, shared.Context.Get(ctx).User.Id).Update()
		return err
	})
}

// 获取消息列表
func (s *userService) GetMessageList(ctx context.Context, input define.UserGetMessageInput) (output *define.UserGetMessageOutput, err error) {
	// 构造返回实例
	output = &define.UserGetMessageOutput{
		Page: input.Page,
		Size: input.Size,
	}
	// 管理员可以查看所有消息
	userId := shared.Context.Get(ctx).User.Id
	if !s.IsAdminShow(ctx, userId) {
		input.UserId = userId
	}
	// 获取回复列表
	replyList, err := Reply.GetList(ctx, define.ReplyGetListInput{
		Page:       input.Page,
		Size:       input.Size,
		TargetType: input.TargetType,
		TargetId:   input.TargetId,
		UserId:     input.UserId, // 注意，这里是input.UserId，不是userId，因为如果是管理员，则input.UserId为空，查询的是所有用户的回复
	})
	if err != nil {
		return nil, err
	}
	output.List = replyList.List
	output.Total = replyList.Total

	// 获取分类统计
	output.Stats, err = s.GetUserStats(ctx, userId)
	if err != nil {
		return nil, err
	}
	return output, nil
}

// 登出
func (s *userService) Logout(ctx context.Context) error {
	return Session.RemoveUser(ctx)
}
